/**
 * Copyright (C) 2005 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.openfire.plugin;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.forms.DataForm;
import org.jivesoftware.openfire.forms.FormField;
import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
import org.xmpp.component.Component;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import javax.naming.NamingEnumeration;
import javax.naming.directory.*;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.SortControl;


import org.jivesoftware.openfire.ldap.LdapManager;
import javax.naming.NamingException;
import java.io.IOException;
import java.io.File;
import java.util.*;

/** 
 * Provides support for Jabber Search
 * (<a href="http://www.xmpp.org/extensions/xep-0055.html">XEP-0055</a>).<p>
 *
 * The basic functionality is to query an information repository 
 * regarding the possible search fields, to send a search query, 
 * and to receive search results. This implementation was primarily designed to use
 * <a href="http://www.xmpp.org/extensions/xep-0004.html">Data Forms</a>, but 
 * also supports non-dataform searches.
 * <p/>
 * Based on the search plugin from <a href="mailto:ryan@version2software.com">Ryan Graham</a>
 *
 * @author Jay Kline, sponsored by Interwise, Inc.
 */
public class LdapSearchPlugin implements Component, Plugin, PropertyEventListener {
    public static final String SERVICENAME = "plugin.ldapsearch.serviceName";
    public static final String SERVICEENABLED = "plugin.ldapsearch.serviceEnabled";
    public static final String SEARCHFIELDS = "ldap.searchFields";
    public static final String MAXRESULTS = "plugin.ldapsearch.maxResults";
    public static final String VIOLATEXEP55 = "plugin.ldapsearch.violateXEP55";
    public static final String USEDATAFORMS = "plugin.ldapsearch.useDataForms";
    public static final String DEFAULTANDSEARCH = "plugin.ldapsearch.defaultAndSearch";
    public static final String SEARCHFILTER = "ldap.searchFilter";
    public static final String EXTRADEBUG = "plugin.ldapsearch.debug";

    private ComponentManager componentManager;
    private PluginManager pluginManager;
    private LdapManager ldapManager;

    private String serviceName;
    private boolean serviceEnabled;
    private int maxResults;
    private boolean violateXEP55;
    private boolean useDataForms;
    private boolean defaultAndSearch;
    private boolean extraDebug;
    private String searchFilter;

    private static String serverName;

    // searchFields = Map<Label.toLowerCase(),Field> 
    private Map<String,String> searchFields;
    // reverseFieldLookup = Map<Field, Label>
    private Map<String, String> reverseFieldLookup = new LinkedHashMap<String, String>();
    // fieldLookup = Map<Unknown Field,Known Field>
    private TreeMap<String, String> fieldLookup = new TreeMap<String, String>(new CaseInsensitiveComparator());

    public LdapSearchPlugin() {
        serviceName = JiveGlobals.getProperty(SERVICENAME, "ldapsearch");
        //titleString = LocaleUtils.getLocalizedString("ldap.user.search.title","ldapsearch");
        //instructionsString = LocaleUtils.getLocalizedString("ldap.user.search.instructions","ldapsearch");
        serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true);

        try {
            maxResults = Integer.parseInt(JiveGlobals.getProperty(MAXRESULTS, "500"));
            if(maxResults < 1) {
                Log.debug("LdapSearchPlugin: invalid number for maxResults");
                maxResults = 500;
            }
        } catch (NumberFormatException nfe) {
            Log.debug("LdapSearchPlugin: invalid number for maxResults");
            maxResults = 500;
        }

        violateXEP55 = JiveGlobals.getBooleanProperty(VIOLATEXEP55, false);
        useDataForms = JiveGlobals.getBooleanProperty(USEDATAFORMS, true);
        defaultAndSearch = JiveGlobals.getBooleanProperty(DEFAULTANDSEARCH, false);
        searchFilter = JiveGlobals.getXMLProperty(SEARCHFILTER);
        extraDebug = JiveGlobals.getBooleanProperty(EXTRADEBUG, false);

        if (extraDebug) {
            Log.debug("LdapSearchPlugin: initializing (extraDebug enabled)");
            Log.debug("LdapSearchPlugin: defaultAndSearch: "+defaultAndSearch);
            Log.debug("LdapSearchPlugin: useDataForms: "+useDataForms);
            Log.debug("LdapSearchPlugin: violateXEP55: "+violateXEP55);
        }

        serverName = XMPPServer.getInstance().getServerInfo().getName();
        ldapManager = LdapManager.getInstance();

        searchFields = new LinkedHashMap<String,String>();
        String fieldList = JiveGlobals.getXMLProperty(SEARCHFIELDS);
        // If the value isn't present, default to to username, name, and email.
        if (fieldList == null) {
            searchFields.put("username", ldapManager.getUsernameField());
            searchFields.put("name", ldapManager.getNameField());
            searchFields.put("email", ldapManager.getEmailField());
            reverseFieldLookup.put(ldapManager.getUsernameField(),"Username");
            reverseFieldLookup.put(ldapManager.getNameField(),"Name");
            reverseFieldLookup.put(ldapManager.getEmailField(),"Email");
        }
        else {
            try {
                for (StringTokenizer i=new StringTokenizer(fieldList, ","); i.hasMoreTokens(); ) {
                    String[] field = i.nextToken().split("/");
                    searchFields.put(field[0].toLowerCase(), field[1]);
                    reverseFieldLookup.put(field[1],field[0]);
                }
            }
            catch (Exception e) {
                Log.error("Error parsing LDAP search fields: " + fieldList, e);
            }
        }


        // Some clients, such as Miranda, are hard-coded to search specific fields,
        // so we map those fields to the fields that Openfire actually supports.
        fieldLookup.put("jid", ldapManager.getUsernameField());
        fieldLookup.put("username", ldapManager.getUsernameField());
        fieldLookup.put("first", ldapManager.getNameField());
        fieldLookup.put("last", ldapManager.getNameField());
        fieldLookup.put("nick", ldapManager.getNameField());
        fieldLookup.put("name", ldapManager.getNameField());
        fieldLookup.put("email", ldapManager.getEmailField());

        // If spaces are present, map them out
        for (String field : reverseFieldLookup.keySet()) {
            if(field.indexOf(' ') > -1) {
                fieldLookup.put(field.replace(" ",""),field);
            }
        }
    }

    public String getName() {
        return pluginManager.getName(this);
    }

    public String getDescription() {
        return pluginManager.getDescription(this);
    }

    public void initializePlugin(PluginManager manager, File pluginDirectory) {
        pluginManager = manager;

        componentManager = ComponentManagerFactory.getComponentManager();
        try {
            componentManager.addComponent(serviceName, this);
        }
        catch (ComponentException e) {
            componentManager.getLog().error(e);
        }
        PropertyEventDispatcher.addListener(this);
    }

    public void initialize(JID jid, ComponentManager componentManager) {
    }

    public void start() {
    }

    public void destroyPlugin() {
        if (extraDebug) {
            Log.debug("LdapSearchPlugin: destroying plugin");
        }
        PropertyEventDispatcher.removeListener(this);
        pluginManager = null;
        try {
            componentManager.removeComponent(serviceName);
            componentManager = null;
        }
        catch (Exception e) {
            componentManager.getLog().error(e);
        }
        serviceName = null;
        searchFields = null;
        serverName = null;
        fieldLookup = null;
        reverseFieldLookup = null;
    }

    public void shutdown() {
    }

    public void processPacket(Packet p) {
        if (p instanceof IQ) {
            IQ packet = (IQ) p;

            Element childElement = (packet).getChildElement();
            String namespace = null;
            if (childElement != null) {
                namespace = childElement.getNamespaceURI();
            }

            if ("jabber:iq:search".equals(namespace)) {
                try {
                    IQ replyPacket = handleIQ(packet);
                    if (replyPacket != null) {
                        componentManager.sendPacket(this, replyPacket);
                    }
                }
                catch (ComponentException e) {
                    componentManager.getLog().error(e);
                }
            }
            else if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
                try {
                    IQ replyPacket = IQ.createResultIQ(packet);

                    Element responseElement = replyPacket
                            .setChildElement("query", "http://jabber.org/protocol/disco#info");
                    responseElement.addElement("identity").addAttribute("category", "directory")
                                                          .addAttribute("type", "user")
                                                          .addAttribute("name", "User Search");
                    responseElement.addElement("feature").addAttribute("var", "jabber:iq:search");

                    componentManager.sendPacket(this, replyPacket);
                }
                catch (ComponentException e) {
                    componentManager.getLog().error(e);
                }
            }
            else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
                try {
                    IQ replyPacket = IQ.createResultIQ(packet);
                    replyPacket.setChildElement("query", "http://jabber.org/protocol/disco#items");
                    componentManager.sendPacket(this, replyPacket);
                }
                catch (ComponentException e) {
                    componentManager.getLog().error(e);
                }
            }
        }
    }

    private IQ handleIQ(IQ packet) {
        if (!serviceEnabled) {
            return replyDisabled(packet);
        }

        if (IQ.Type.get.equals(packet.getType())) {
            return processGetPacket(packet);
        }
        else if (IQ.Type.set.equals(packet.getType())) {
            return processSetPacket(packet);
        }
        else if (IQ.Type.result.equals(packet.getType()) || IQ.Type.error.equals(packet.getType())) {
            // Ignore
        }
        else {
            // Unknown type was sent so return an error
            IQ reply = new IQ(IQ.Type.error, packet.getID());
            reply.setFrom(packet.getTo());
            reply.setTo(packet.getFrom());
            reply.setError(PacketError.Condition.bad_request);
            return reply;
        }

        return null;
    }

    private IQ replyDisabled(IQ packet) {
        IQ replyPacket = IQ.createResultIQ(packet);
        Element reply = replyPacket.setChildElement("query", "jabber:iq:search");
        XDataFormImpl unavailableForm = new XDataFormImpl(DataForm.TYPE_CANCEL);
        unavailableForm.setTitle(LocaleUtils.getLocalizedString("ldap.user.search.title","ldapsearch"));
        unavailableForm.addInstruction(LocaleUtils.getLocalizedString("ldap.user.search.service_unavailable","ldapsearch"));
        reply.add(unavailableForm.asXMLElement());

        return replyPacket;
    }

    private IQ processGetPacket(IQ packet) {
        IQ replyPacket = IQ.createResultIQ(packet);

        Element queryResult = DocumentHelper.createElement(QName.get("query", "jabber:iq:search"));

        // non-data form
        queryResult.addElement("instructions").addText(LocaleUtils.getLocalizedString("ldap.user.search.instructions","ldapsearch"));


        XDataFormImpl searchForm = new XDataFormImpl(DataForm.TYPE_FORM);
        searchForm.setTitle(LocaleUtils.getLocalizedString("ldap.user.search.title","ldapsearch"));
        searchForm.addInstruction(LocaleUtils.getLocalizedString("ldap.user.search.instructions","ldapsearch"));

        XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
        field.setType(FormField.TYPE_HIDDEN);
        field.addValue("jabber:iq:search");
        searchForm.addField(field);


        for (String searchField : reverseFieldLookup.values()) {
            // non-data form. XEP-0055 says only first, last, nick, email, and jid are allowed
            if("first".equals(searchField.toLowerCase()) || "last".equals(searchField.toLowerCase()) ||
               "nick".equals(searchField.toLowerCase())  || "email".equals(searchField.toLowerCase()) ||
                "jid".equals(searchField.toLowerCase()) ) {
                if (violateXEP55) {
                    queryResult.addElement(searchField);
                } else {
                    queryResult.addElement(searchField.toLowerCase());
                }
            }
            else if(violateXEP55) {
                //spaces need to be removed, if any
                queryResult.addElement(searchField.replace(" ",""));
            }

            field = new XFormFieldImpl(searchFields.get(searchField.toLowerCase()));
            field.setType(FormField.TYPE_TEXT_SINGLE);
            field.setLabel(searchField);
            field.setRequired(false);
            searchForm.addField(field);
        }

        field = new XFormFieldImpl("ANDOR");
        field.setType(FormField.TYPE_LIST_SINGLE);
        field.addOption("AND","AND");
        field.addOption("OR","OR");
        field.addValue("OR");
        field.setRequired(true);
        field.setLabel("Search Type");
        searchForm.addField(field);

        if (useDataForms) {
            queryResult.add(searchForm.asXMLElement());
        }

        replyPacket.setChildElement(queryResult);

        return replyPacket;
    }

    private IQ processSetPacket(IQ packet) {

        Set<Attributes> users = new HashSet<Attributes>();
        ArrayList<String> fields = new ArrayList<String>();
        fields.addAll(getFilteredSearchFields());

        // Construct the search
        StringBuilder filter = new StringBuilder();
        filter.append("(");

        Element incomingForm = packet.getChildElement();
        boolean isDataFormQuery = (incomingForm.element(QName.get("x", "jabber:x:data")) != null);
        boolean andSearch = defaultAndSearch; // do AND search, instead of OR

        Hashtable<String, String> searchList = extractSearchQuery(incomingForm);
        if(searchList.containsKey("ANDOR")) {
            String value = searchList.remove("ANDOR");
            if(value.equals("AND")) {
                Log.debug("LdapSearchPlugin: AND search requested");
                andSearch = true;
            } else if(value.equals("OR")) {
                Log.debug("LdapSearchPlugin: OR search requested");
                andSearch = false;
            }
        }
        if (andSearch) {
            filter.append("&");
        }
        else {
            filter.append("|");
        }

        Enumeration<String> searchIter = searchList.keys();
        while (searchIter.hasMoreElements()) {
            String field = searchIter.nextElement();
            String query = searchList.get(field);
            if (query == null || query.equals("") || field.equals("FORM_TYPE") ) {
                continue;
            }
            if (isDataFormQuery) {
                if (query.length() > 0 && !query.equals("jabber:iq:search") && searchFields.containsValue(field)) {
                    //append a * to the end to make all queries wildcarded, to follow the behavior in the LdapUserProvider
                    filter.append("(").append(field).append("=").append(query);
                    if(!query.endsWith("*")) {
                        filter.append("*");
                    }
                    filter.append(")");
                }
            }
            else {
                Log.debug("LdapSearchPlugin: Non-dataform search of: "+field+":"+searchFields.get(field.toLowerCase())+":"+query);
                if (query.length() > 0 && !query.equals("jabber:iq:search") && searchFields.containsKey(field.toLowerCase())) {
                    // escape ( and ) to prevent mallicous queries
                    String value = query.replace("(","\\(").replace(")","\\)");
                    filter.append("(").append(searchFields.get(field.toLowerCase())).append("=").append(query);

                    //append a * to the end to make all queries wildcarded, to follow the behavior in the LdapUserProvider
                    if(!query.endsWith("*")) {
                        filter.append("*");
                    }
                    filter.append(")");
                } else { 
                    //its possible that spaces were removed
                }
            }
        }
        filter.append(")");

        if (searchFilter != null) {
            filter.insert(0,"(&("+searchFilter+")");
            filter.append(")");
        }

        Log.debug("LdapSearchPlugin: filter is  "+filter.toString());
        if(fields.size() < 1) {
            //empty query
        }
        Log.debug("LdapSearchPlugin: Requested fields: "+fields);

        fields.add(ldapManager.getUsernameField());

        IQ replyPacket = null;

        try {
            LdapContext ctx1 = ldapManager.getContext();
            LdapContext ctx2 = null;
            // Sort by username
            Control[] searchControl = new Control[]{
                    new SortControl(new String[]{ldapManager.getUsernameField()}, Control.NONCRITICAL)
            };
            ctx1.setRequestControls(searchControl);
            // Search for the dn based on the username.
            SearchControls searchControls = new SearchControls();
            // See if recursive searching is enabled. Otherwise, only search one level.
            if (ldapManager.isSubTreeSearch()) {
                searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            }
            else {
                searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
            }
            searchControls.setReturningAttributes( fields.toArray(new String[0]) );

            NamingEnumeration<SearchResult> answer = ctx1.search("", filter.toString(), searchControls);
            NamingEnumeration<SearchResult> answer2 = null;
            String alternateBaseDN = ldapManager.getAlternateBaseDN();
            if(alternateBaseDN != null) {
                ctx2 = ldapManager.getContext(alternateBaseDN);
                ctx2.setRequestControls(searchControl);
                answer2 = ctx2.search("", filter.toString(), searchControls);
            }

            for (int i = 0; i < maxResults; i++) {
                if (answer.hasMoreElements()) {
                    // Get the next userID.
                    Attributes result = answer.next().getAttributes();
                    if (extraDebug) {
                        Log.debug("LdapSearchPlugin: found "+(String)result.get(ldapManager.getUsernameField()).get());
                    }
                    users.add(result);
                }
                else if (alternateBaseDN != null && answer2.hasMoreElements())
                {
                    // Get the next userID.
                    Attributes result = answer2.next().getAttributes();
                    if (extraDebug) {
                        Log.debug("LdapSearchPlugin: found "+(String)result.get(ldapManager.getUsernameField()).get());
                    }
                    users.add(result);
                }
                else {
                    break;
                }
            }
            Log.debug("LdapSearchPlugin: search complete.");
        } catch (NamingException ne) {
                Log.debug("LdapSearchPlugin: Naming expection: "+ne.toString());
        } catch (IOException ioe) {
                Log.debug("LdapSearchPlugin: IO expection: "+ioe.toString());

        }
        Log.debug("LdapSearchPlugin: search found "+users.size()+" users.");

        if (isDataFormQuery) {
            replyPacket = replyDataFormResult(users, packet);
        }
        else {
            replyPacket = replyNonDataFormResult(users, packet);
        }

        return replyPacket;

    }

    /**
     * Constructs a XForm that is returned as an IQ packet that contains the search results.
     * 
     * @param users set of users that will be used to construct the search results
     * @param packet the IQ packet sent by the client
     * @return the iq packet that contains the search results
     */
    private IQ replyDataFormResult(Set<Attributes> users, IQ packet) {
        XDataFormImpl searchResults = new XDataFormImpl(DataForm.TYPE_RESULT);

        XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
        field.setType(FormField.TYPE_HIDDEN);
        searchResults.addField(field);

        field = new XFormFieldImpl("jid");
        field.setLabel("JID");
        searchResults.addReportedField(field);


        for (String fieldName : reverseFieldLookup.values()) {
            field = new XFormFieldImpl(searchFields.get(fieldName.toLowerCase()));
            field.setLabel(fieldName);
            searchResults.addReportedField(field);
        }

        if (extraDebug) {
            Log.debug("LdapSearchPlugin: Assembling dataform result");
        }

        for (Attributes user : users) {
            String username = "";
            try {
                username = JID.unescapeNode((String)user.get(ldapManager.getUsernameField()).get());
            } catch (NamingException ne) {
                Log.warn("LdapSearchPlugin: username field empty! "+user.get(ldapManager.getUsernameField()).getID());
                continue;
            }


            ArrayList<XFormFieldImpl> items = new ArrayList<XFormFieldImpl>();

            XFormFieldImpl fieldJID = new XFormFieldImpl("jid");
            fieldJID.addValue(username + "@" + serverName);
            items.add(fieldJID);

            if (extraDebug) {
                Log.debug("LdapSearchPlugin: +jid: "+username);
            }
            for (String fieldName : searchFields.values()) {
                field = new XFormFieldImpl(fieldName);
                Attribute attribute = user.get(fieldName);
                String value = "";
                if (attribute == null) {
                    value = "";
                }
                else {
                    try {
                        value = (String)attribute.get();
                    } catch (NamingException ne) {
                        value = "";
                    } catch (NoSuchElementException nsee) {
                        value = "";
                    }
                }
                field.addValue(value);
                if (extraDebug) {
                    Log.debug("LdapSearchPlugin: "+fieldName+": "+value);
                }

                items.add(field);
                searchResults.addItemFields(items);
            }
        }
        IQ replyPacket = IQ.createResultIQ(packet);
        Element reply = replyPacket.setChildElement("query", "jabber:iq:search");
        reply.add(searchResults.asXMLElement());

        return replyPacket;
    }


    /**
     * Constructs a query that is returned as an IQ packet that contains the search results.
     * 
     * @param users set of users that will be used to construct the search results
     * @param packet the IQ packet sent by the client
     * @return the iq packet that contains the search results
     */
    private IQ replyNonDataFormResult(Set<Attributes> users, IQ packet) {
        IQ replyPacket = IQ.createResultIQ(packet);
        Element replyQuery = replyPacket.setChildElement("query", "jabber:iq:search");

        if (extraDebug) {
            Log.debug("LdapSearchPlugin: Assembling non-dataform result");
        }


        for (Attributes user : users) {

            Element item = replyQuery.addElement("item");
            String username = "";
            try {
                username = JID.unescapeNode((String)user.get(ldapManager.getUsernameField()).get());
            } catch (NamingException ne) {
                Log.warn("LdapSearchPlugin: username field empty! "+user.get(ldapManager.getUsernameField()).getID());
                continue;
            }


            item.addAttribute("jid", username + "@" + serverName);

            if (extraDebug) {
                Log.debug("LdapSearchPlugin: +jid: "+username);
            }
            for (String field : reverseFieldLookup.keySet()) {
                Attribute attribute = user.get(field);
                String value = "";
                if (attribute == null) {
                    value = "";
                }
                else {
                    try {
                        value = (String)attribute.get();
                    } catch (NamingException ne) {
                        value = "";
                    } catch (NoSuchElementException nsee) {
                        value = "";
                    }
                }

                if (violateXEP55) {
                    Element element = item.addElement(reverseFieldLookup.get(field));
                    element.addText(value);
                        if (extraDebug) {
                            Log.debug("LdapSearchPlugin: "+field+": "+value);
                        }
                }
                else if("first".equals(reverseFieldLookup.get(field).toLowerCase()) ||
                    "last".equals(reverseFieldLookup.get(field).toLowerCase()) ||
                    "nick".equals(reverseFieldLookup.get(field).toLowerCase()) ||
                    "email".equals(reverseFieldLookup.get(field).toLowerCase()) ) {

                    Element element = item.addElement(reverseFieldLookup.get(field).toLowerCase());
                    element.addText(value);
                    if (extraDebug) {
                        Log.debug("LdapSearchPlugin: "+field+": "+value);
                    }
                }
            }
        }

        return replyPacket;
    }


   private Hashtable<String, String> extractSearchQuery(Element incomingForm) {
        Hashtable<String, String> searchList = new Hashtable<String, String>();
        Element form = incomingForm.element(QName.get("x", "jabber:x:data"));
        if (form == null) {
            Log.debug("LdapSearchPlugin: non form search; parsing query");
            //since not all clients request which fields are available for searching
            //attempt to match submitted fields with available search fields
            Iterator iter = incomingForm.elementIterator();
            while (iter.hasNext()) {
                Element element = (Element) iter.next();
                String name = element.getName();
                if (searchFields.containsKey(name.toLowerCase()) ) {
                    Log.debug("LdapSearchPlugin: "+name+"("+searchFields.get(name.toLowerCase())+") found in searchFields. Value: "+element.getText());
                    searchList.put(name,element.getText());
                }
                else if (fieldLookup.containsKey(name)) {
                    //make best effort to map the fields submitted by
                    //the client to those that Openfire can search
                    Log.debug("LdapSearchPlugin: Last ditch: "+name+"("+fieldLookup.get(name)+") found. Value: "+element.getText());
                    searchList.put(fieldLookup.get(name), element.getText());
                }
                else if (fieldLookup.containsKey(name.toLowerCase())) {
                    Log.debug("LdapSearchPlugin: Last ditch: "+name+"("+fieldLookup.get(name.toLowerCase())+") found. Value: "+element.getText());
                    searchList.put(fieldLookup.get(name.toLowerCase()), element.getText());
                }
                else {
                    Log.debug("LdapSearchPlugin: "+name+" not found. Value: "+element.getText());
                }
            }
        }
        else {
            List<String> searchFields = new ArrayList<String>();
            String search = "";

            Iterator fields = form.elementIterator("field");
            while (fields.hasNext()) {
                Element searchField = (Element) fields.next();

                String field = searchField.attributeValue("var");
                String value = "";
                if (searchField.element("value") != null) {
                    value = searchField.element("value").getTextTrim();
                }
                searchList.put(field,value);
            }
        }
        return searchList;
    }

    /**
     * Returns the service name of this component, which is "search" by default.
     *
     * @return the service name of this component.
     */
    public String getServiceName() {
        return serviceName;
    }

    /**
     * Sets the service name of this component, which is "search" by default. If the name
     * is different than the existing name the plugin will remove itself from the ComponentManager
     * and then add itself back using the new name.
     *
     * @param name the service name of this component.
     */
    public void setServiceName(String name) {
        changeServiceName(name);
        JiveGlobals.setProperty(SERVICENAME, name);
    }

    /**
     * @return true if search service is enabled.
     */
    public boolean getServiceEnabled() {
        return serviceEnabled;
    }

    /**
     * Enables or disables the search service. When disabled, when a client tries 
     * to do a search they will receive an XForm informing that the service is
     * unavailable.
     *
     * @param enabled true if group permission checking should be disabled.
     */
    public void setServiceEnabled(boolean enabled) {
        serviceEnabled = enabled;
        JiveGlobals.setProperty(SERVICEENABLED, enabled ? "true" : "false");
    }

    public boolean getViolateXEP55() {
        return violateXEP55;
    }

    public void setViolateXEP55(boolean violate) {
        violateXEP55 = violate;
        JiveGlobals.setProperty(VIOLATEXEP55, violate ? "true" : "false");
    }

    public boolean getUseDataForms() {
        return useDataForms;
    }

    public void setUseDataForms(boolean useDataForms) {
        this.useDataForms = useDataForms;
        JiveGlobals.setProperty(USEDATAFORMS, useDataForms ? "true" : "false");
    }

    public int getMaxResults() {
        return maxResults;
    }

    public int setMaxResults(int max) {
        if (max > 0) {
            maxResults = max;
            JiveGlobals.setProperty(MAXRESULTS, ""+max);
        }
        return maxResults;
    }

    public void setDefaultAndSearch(boolean andSearch) {
        defaultAndSearch = andSearch;
        JiveGlobals.setProperty(DEFAULTANDSEARCH, defaultAndSearch ? "true" : "false");
    }

    public boolean getDefaultAndSearch() {
        return defaultAndSearch;
    }

    /**
     * Returns the collection of searchable field names that does not include the fields
     * listed in the EXCLUDEDFIELDS property list.
     */
    public Collection<String> getFilteredSearchFields() {
       return searchFields.values();
    }

    public void propertySet(String property, Map params) {
        if (property.equals(SERVICEENABLED)) {
            this.serviceEnabled = Boolean.parseBoolean((String)params.get("value"));
        }
        else if (property.equals(SERVICENAME)) {
            changeServiceName((String)params.get("value"));
        }
        else if (property.equals(MAXRESULTS)) {
            try {
                maxResults = Integer.parseInt((String)params.get("value"));
            } catch (NumberFormatException nfe) {
                Log.debug("LdapSearchPlugin: invalid number for maxResults");
                maxResults = 500;
            }
        }
        else if (property.equals(VIOLATEXEP55)) {
            violateXEP55 = Boolean.parseBoolean((String)params.get("value"));
        }
        else if (property.equals(USEDATAFORMS)) {
            useDataForms = Boolean.parseBoolean((String)params.get("value"));
        }
        else if (property.equals(DEFAULTANDSEARCH)) {
            defaultAndSearch = Boolean.parseBoolean((String)params.get("value"));
        }
        else if (property.equals(EXTRADEBUG)) {
            extraDebug = Boolean.parseBoolean((String)params.get("value"));
        }
    }

    public void propertyDeleted(String property, Map params) {
        if (property.equals(SERVICEENABLED)) {
            this.serviceEnabled = true;
        }
        else if (property.equals(SERVICENAME)) {
            changeServiceName("ldapsearch");
        }
        else if (property.equals(SEARCHFIELDS)) {
           searchFields = new HashMap<String,String>();
        }
        else if (property.equals(MAXRESULTS)) {
            maxResults = 500;
        }
        else if (property.equals(VIOLATEXEP55)) {
            violateXEP55 = false;
        }
        else if (property.equals(USEDATAFORMS)) {
            useDataForms = true;
        }
        else if (property.equals(DEFAULTANDSEARCH)) {
            defaultAndSearch = false;
        }
        else if (property.equals(EXTRADEBUG)) {
            extraDebug = false;
        }
    }

    public void xmlPropertySet(String property, Map params) {
        if (property.equals(SEARCHFIELDS)) {
            searchFields = new LinkedHashMap<String,String>();
            String fieldList = (String)params.get("value");
            // If the value isn't present, default to to username, name, and email.
            if (fieldList == null) {
                searchFields.put("username", ldapManager.getUsernameField());
                searchFields.put("name", ldapManager.getNameField());
                searchFields.put("email", ldapManager.getEmailField());
                reverseFieldLookup.put(ldapManager.getUsernameField(),"Username");
                reverseFieldLookup.put(ldapManager.getNameField(),"Name");
                reverseFieldLookup.put(ldapManager.getEmailField(),"Email");
            }
            else {
                try {
                    for (StringTokenizer i=new StringTokenizer(fieldList, ","); i.hasMoreTokens(); ) {
                        String[] field = i.nextToken().split("/");
                        searchFields.put(field[0].toLowerCase(), field[1]);
                        reverseFieldLookup.put(field[1],field[0]);
                    }
                }
                catch (Exception e) {
                    Log.error("Error parsing LDAP search fields: " + fieldList, e);
                }
            }

            fieldLookup = new TreeMap<String, String>(new CaseInsensitiveComparator());
            fieldLookup.put("jid", ldapManager.getUsernameField());
            fieldLookup.put("username", ldapManager.getUsernameField());
            fieldLookup.put("first", ldapManager.getNameField());
            fieldLookup.put("last", ldapManager.getNameField());
            fieldLookup.put("nick", ldapManager.getNameField());
            fieldLookup.put("name", ldapManager.getNameField());
            fieldLookup.put("email", ldapManager.getEmailField());

            // If spaces are present, map them out
            for (String field : reverseFieldLookup.keySet()) {
                if(field.indexOf(' ') > -1) {
                    fieldLookup.put(field.replace(" ",""),field);
                }
            }
        }
        else if (property.equals(SEARCHFILTER)) {
            searchFilter = (String)params.get("value");
        }

    }

    public void xmlPropertyDeleted(String property, Map params) {
        if (property.equals(SEARCHFIELDS)) {
            searchFields = new HashMap<String,String>();
            searchFields.put("username", ldapManager.getUsernameField());
            searchFields.put("name", ldapManager.getNameField());
            searchFields.put("email", ldapManager.getEmailField());
            reverseFieldLookup.put(ldapManager.getUsernameField(),"Username");
            reverseFieldLookup.put(ldapManager.getNameField(),"Name");
            reverseFieldLookup.put(ldapManager.getEmailField(),"Email");
            fieldLookup = new TreeMap<String, String>(new CaseInsensitiveComparator());
            fieldLookup.put("jid", ldapManager.getUsernameField());
            fieldLookup.put("username", ldapManager.getUsernameField());
            fieldLookup.put("first", ldapManager.getNameField());
            fieldLookup.put("last", ldapManager.getNameField());
            fieldLookup.put("nick", ldapManager.getNameField());
            fieldLookup.put("name", ldapManager.getNameField());
            fieldLookup.put("email", ldapManager.getEmailField());
        } 
        else if (property.equals(SEARCHFILTER)) {
            searchFilter = null;
        }

    }

    private void changeServiceName(String serviceName) {
        if (serviceName == null) {
            throw new NullPointerException("Service name cannot be null");
        }

        if (this.serviceName.equals(serviceName)) {
            return;
        }

        // Re-register the service.
        try {
            componentManager.removeComponent(this.serviceName);
        }
        catch (Exception e) {
            componentManager.getLog().error(e);
        }

        try {
            componentManager.addComponent(serviceName, this);
        }
        catch (Exception e) {
            componentManager.getLog().error(e);
        }

        this.serviceName = serviceName;
    }

    private class CaseInsensitiveComparator implements Comparator<String> {
        public int compare(String s1, String s2) {
            return s1.compareToIgnoreCase(s2);
        }
    }

    private String removeNull(String s) {
        if (s == null) {
            return "";
        }

        return s.trim();
    }
}
